mpvue 构建优化记录

本文在mpvue提供的默认项目模板的基础上,对webpack构建过程进行了优化,使其更加符合大型的小程序项目。

1. 清理废弃页面

随着产品的快速迭代,项目里的部分页面会逐渐下线。为了避免下线的页面突然又要重新上线,开发同学一般会把下线页面的代码保留,仅仅在app.json里移除掉相应的页面。

但细心的同学可能会发现,尽管已经在app.json里移除了相应页面的路径,在编译出来的dist文件夹仍然能发现已下线页面的身影,这是为什么呢?

答案就在webpack.base.config.js里的这段代码里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
function getEntry (rootSrc) {
var map = {};
glob.sync(rootSrc + '/pages/**/main.js')
.forEach(file => {
var key = relative(rootSrc, file).replace('.js', '');
map[key] = file;
})
return map;
}
const appEntry = { app: resolve('./src/main.js') }
const pagesEntry = getEntry(resolve('./src'), 'pages/**/main.js')
const entry = Object.assign({}, appEntry, pagesEntry)

可以看到mpvue的做法是去遍历src/pages目录下的所有main.js文件, 将他们所在的路径作为webapck的entry,参与编译。因此即使的已经从app.json里移除了下线页面,只要他们的main.js还在,就还会被编译。

那么最简单的解决方法显而易见——将下线页面的main.js改名,例如改成main-old.js就不会被编译了。

可是手动改文件名来下线页面实在是太蠢了,有没有更自动一点的方法呢?答案是肯定的。

我们能看到,其实我们需要编译哪些页面,全都已经写在app.json里了。(什么?你说你app.json里也没有删掉下线页面?那你可以被拖出去打了)

app.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"pages": [
"pages/index/home/main",
// ...
],
"subpackages": [
{
"root": "pages/sub",
"pages": [
"card/main"
]
}
]
// ...
}

那么其实我们只要根据app.json里的页面来改写getEntry方法就好了,改写完后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function getEntry(rootSrc, globPattern) {
// 提取app.json里的页面清单
const appJson = require('../src/app.json')
// 复制一份,避免改写require缓存里的app.json数据
const appJsonPages = appJson.pages.slice()
// 处理分包页面
if (appJson.subpackages) {
appJson.subpackages.forEach(package => {
package.pages.forEach(subPage => appJsonPages.push(`${package.root}/${subPage}`))
})
}
var map = {};
glob.sync(globPattern)
.forEach(file => {
var key = relative(rootSrc, file).replace('.js', '');
// relative取出来的路径的分隔符跟系统有关,而app.json则是固定用/分割
const keyWithSep = key.split(path.sep).join('/')
if (appJsonPages.includes(keyWithSep)) {
// 在app.json里的页面才会被放进entry
map[key] = file;
} else {
// 否则在控制台提示
console.log(`[:exclamation:注意:exclamation:] 页面 ${key} 不在app.json中,没有被编译`)
}
})
return map;
}

这样我们就只需要对app.json进行删减,就能够轻松下线页面啦。

2. 处理分包的公共代码

小程序其实可以看作是一个多页应用,在启动时只会下载主包的页面及公共代码,当跳转到分包页面时,才会开始下载分包页面的代码。

更多分包的介绍详见官方文档:分包加载 | 微信开放文档

在某次编译的时候,微信开发者工具突然提示common/vendor.js文件体积大于500KB, 不会被执行ES6转ES5。令人奇怪的是最近一段时间主包代码并没有什么大改动,主要都是在分包新增页面,为什么公共的common/vendor.js体积会变大呢?

使用webpack-bundle-analyzer查看了一下common/vendor.js的构成,意外的发现里面居然包含了不少分包的公共代码!本来使用分包,就是为了减小主包体积,加快启动速度。结果现在主包里却包含了主包里不会执行的分包代码,显然是不符合我们预期的。

老规矩,直接去看webpack相应的配置:
查阅资料得知,common/vendor.js是webpack插件CommonsChunkPlugin用来提取公共代码的(吐槽一句:该插件已经在webpack4中被移除,可惜没人维护的mpvue一直没支持webpack4)。

而默认模板里commonChunksPlugin是这么写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
// webpack.dev.config.js
new webpack.optimize.CommonsChunkPlugin({
name: 'common/vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf('node_modules') >= 0
) || count > 1
}
}),

上面的代码会将引用到的npm包或者引用次数>1的代码提取到common/vendor.js里,这也就是为什么common/vendor.js会包含分包代码的原因了——因为分包里存在引用次数>1的代码。

我们的改造方向其实也很清楚:只要将分包的公共代码,放回分包里面就好了。

查询CommonsChunkPlugin的文档可知,该插件有一个可选参数chunks,用来指定提取公共代码的chunks范围。

什么是chunk呢? 简单来说就是webpack里通过entry解析出来的一块相互依赖的资源,在小程序的场景下,可以简单将chunk看作小程序的页面。

如果你对上文的getEntry方法有印象,就能发现其实entry这个对象里的key就是这个页面的chunk name。如果你没有魔改过相关代码,那么页面的chunk name同时也等于它的path。因此,我们只要根据分包的构成,新增多个CommonsChunkPlugin,将他们的chunks字段填上该分包的页面即可。

改良后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// webpack.dev.config.js/webpack.prod.config.js
const appJson = require('../src/app.json')
const subpackages = {}
// 预处理一下分包的数据,变成 {分包根路径:分包页面数组} 的格式
if (appJson.subpackages) {
appJson.subpackages.forEach(package => {
subpackages[package.root] = []
package.pages.forEach(subPage => subpackages[package.root].push(path.join(package.root, subPage)))
})
}
const subPackageCommonChunks = Object.keys(subpackages).map(sub =>
new webpack.optimize.CommonsChunkPlugin({
// 在分包目录下提取一个叫common的公共文件,注意使用path.join,兼容mac/windows
name: path.join(sub, 'common'),
// 分包下的页面数组
chunks: subpackages[sub],
// 引用次数大于等于2就被划进common
minChunks: 2,
})
)
// 最后插在原本的commonsChunkPlulgin前
module.exports = merge(baseWebpackConfig, {
// ...
plugins: [
// ...
// 插在这里即可
...subPackageCommonChunks,
new webpack.optimize.CommonsChunkPlugin({
name: 'common/vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf('node_modules') >= 0
) || count > 1
}
})
]
})

这时候再用webpack-bundle-analyzer看一下bundle构成,就会发现common/vendor.js里已经基本没什么分包的代码啦~

小结

以上就是在使用mpvue开发过程过中对webpack构建过程作出的优化,分享给同样在使用mpvue做开发的小伙伴们。

其实构建优化这里可以做的还有很多,例如公共样式的提取、将仅在分包使用的npm包打包进分包以及app.json新增不存在的路径时自动创建目录等,如果你有好的想法,欢迎一起来交流学习~

最后说一句,如果你是准备开始一个新项目,请出门左转直接用原生语法写小程序吧,框架的坑实在太深了;(